﻿using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using regxExtensions;

namespace regx
{
    public class LineMatches
    { //=======================================================================
        
        // This is a data bag. Ignore the warning about visible fields.
        [SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] public string           SourceLine   = null;
        [SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] public string           ReplacedLine = null;
        [SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] public MatchCollection  Matches      = null;
        [SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")] public ulong            LineNumber   = 0;
    }

    public class RegexResultFormatter
    {   //=====================================================================
        private static class Tokens
        {   //=================================================================
            public const string SourceName      = @"%f";   
            
            public const string InputLine       = @"%i";
            public const string ReplacedLine    = @"%r";
            public const string LineNumber      = @"%lnum";   
            
            public const string MatchPosition   = @"%mpos";   
            public const string MatchValue      = @"%mval";   
            public const string MatchId         = @"%mid";
            public const string MatchGroups     = @"%mgroups";
            
            public const string GroupId         = @"%gid"; 
            public const string GroupValue      = @"%gval";
            public const string GroupPosition   = @"%gpos";
            
            public const string CaptureId       = @"%cid"; 
            public const string CaptureValue    = @"%cval";
            public const string CapturePosition = @"%cpos";
            
            public const string Counter         = @"%#";
            
            public const string Percent         = @"%%";
            public const string Newline         = @"%nl";   
            public const string Tab             = @"%tab";

            public const string EscapeCharacter = @"%";
        }

        private static class FormatStrings
        {   //=================================================================
            public const string FullLine     = Tokens.InputLine;
            public const string LineNumber   = Tokens.LineNumber + @": " + Tokens.InputLine;
            public const string FileAndLine  = Tokens.SourceName + @"(" + Tokens.LineNumber + @"): " + Tokens.InputLine;
            
            public const string ReplacedLine        = Tokens.ReplacedLine;
            public const string ReplacedLineNumber  = Tokens.LineNumber + @": " + Tokens.ReplacedLine;
            public const string ReplacedFileAndLine = Tokens.SourceName + @"(" + Tokens.LineNumber + @"): " + Tokens.ReplacedLine;
        }
        
        private enum Detail {Line, Match, Group, Capture}

        private string   _format      = null;
        private Detail   _detail      = Detail.Line;
        private string   _source      = null;
        private string[] _formatparts = null;
        private Regex    _rx          = null;
        private int      _count       = 0;
        
        private OutputSelection _selection = OutputSelection.Matched;
        
        private const string _tokenrx = "(" + Tokens.Percent           + ")"
                                     + "|(" + Tokens.CaptureId         + ")"
                                     + "|(" + Tokens.CapturePosition   + ")"
                                     + "|(" + Tokens.CaptureValue      + ")"
                                     + "|(" + Tokens.Counter           + ")"
                                     + "|(" + Tokens.GroupId           + ")"
                                     + "|(" + Tokens.GroupPosition     + ")"
                                     + "|(" + Tokens.GroupValue        + ")"
                                     + "|(" + Tokens.InputLine         + ")"
                                     + "|(" + Tokens.LineNumber        + ")"
                                     + "|(" + Tokens.MatchPosition     + ")"
                                     + "|(" + Tokens.MatchValue        + ")"
                                     + "|(" + Tokens.MatchGroups       + ")"
                                     + "|(" + Tokens.Newline           + ")"
                                     + "|(" + Tokens.ReplacedLine      + ")"
                                     + "|(" + Tokens.SourceName        + ")"
                                     + "|(" + Tokens.Tab               + ")"
                                     + "|(" + Tokens.MatchId           + ")";


        public RegexResultFormatter(Regex regex, string Format) : this(regex, Format, string.Empty) {}
        public RegexResultFormatter(Regex regex, string Format, string SourceName)  
        {   //-----------------------------------------------------------------
            _rx     = regex;
            _source = SourceName ?? string.Empty;
            _format = Format;
            _detail = _format.Contains(Tokens.CaptureId)       ? Detail.Capture
                    : _format.Contains(Tokens.CapturePosition) ? Detail.Capture
                    : _format.Contains(Tokens.CaptureValue)    ? Detail.Capture

                    : _format.Contains(Tokens.GroupId)         ? Detail.Group
                    : _format.Contains(Tokens.GroupPosition)   ? Detail.Group
                    : _format.Contains(Tokens.GroupValue)      ? Detail.Group
                    
                    : _format.Contains(Tokens.MatchPosition)   ? Detail.Match
                    : _format.Contains(Tokens.MatchValue)      ? Detail.Match
                    : _format.Contains(Tokens.MatchGroups)     ? Detail.Match
                    : _format.Contains(Tokens.MatchId)         ? Detail.Match
                    
                    : Detail.Line;

            _formatparts = Regex.Split(_format, _tokenrx);
        }

        public RegexResultFormatter(OutputDetails Details, OutputSelection Selection, bool Replacing) : this(Details, Selection, Replacing, string.Empty) {}
        public RegexResultFormatter(OutputDetails Details, OutputSelection Selection, bool Replacing, string SourceName)
        {   //-----------------------------------------------------------------
            _source    = SourceName ?? string.Empty;
            _selection = Selection;
            _format = Replacing  ? ((Details == OutputDetails.None)        ? FormatStrings.ReplacedLine        
                                 :  (Details == OutputDetails.LineNumber)  ? FormatStrings.ReplacedLineNumber  
                                 :  (Details == OutputDetails.FileAndLine) ? FormatStrings.ReplacedFileAndLine
                                 :                                           FormatStrings.ReplacedLineNumber)  // Default for "Smart"
                /* !Replacing */ : ((Details == OutputDetails.None)        ? FormatStrings.FullLine        
                                 :  (Details == OutputDetails.LineNumber)  ? FormatStrings.LineNumber  
                                 :  (Details == OutputDetails.FileAndLine) ? FormatStrings.FileAndLine
                                 :                                           FormatStrings.FileAndLine);        // Default for "Smart"
            _formatparts = Regex.Split(_format, _tokenrx);
        }        
        
        public void ProcessLineMatches(LineMatches lm, Action<string> fn) {foreach (string s in FormatLineMatches(lm)) fn(s);}

        public IEnumerable<string> FormatLineMatches(LineMatches lm)
        {   //-----------------------------------------------------------------
            // The closure using local variables is to make the code below cleaner. It was already kind of messy with
            // nested if statements galore. Sadly, we can't use the local variable as the iterator in a foreach loop,
            // so we have to remember to assign.
            Match   match     = null;
            int     matchId   = 0;
            Group   group     = null;
            string  groupname = null;
            Capture capture   = null;
            int     captureId = 0;
            Func<string> format = () => {return FormatLine(lm, match, matchId, group, groupname, capture, captureId);};

            if (_detail == Detail.Line)
            {
                // For line-oriented results, we use _selection to check whether to show the line
                switch (_selection)
                {
                    case OutputSelection.All:                                  yield return format();   break;
                    case OutputSelection.Matched:   if (lm.Matches.Count > 0)  yield return format();   break;
                    case OutputSelection.Unmatched: if (lm.Matches.Count == 0) yield return format();   break;
                }
            }
            else
            {
                // For format strings that need Match, Group, and Capture we only show matching lines, so _selection is irrelevant
                foreach (Match matchiterator in from Match m in lm.Matches where m.Success == true select m)
                {
                    // There is no way to get the position in the Matches collection from the Match, so 
                    // we just count up
                    matchId += 1;
                    match = matchiterator;  // Assigned so format() can use the local
                    if (_detail == Detail.Match)
                    {
                        yield return format();
                    }
                    else
                    {
                        // Skip the first groupname. It's "0" and represents the match
                        foreach (string groupnameiterator in _rx.GetGroupNames().Skip(1))
                        {
                            // There is no way to get the name of a group from the Group object,
                            // so we loop through the GroupNamesCollection and index into the Groups
                            // collection. It's annoying.
                            groupname = groupnameiterator;
                            group = match.Groups[groupname];    // Assigned so format() can use the local
                            if (group.Success)
                            {
                                if (_detail == Detail.Group)
                                {
                                    yield return format();
                                }
                                else
                                {
                                    // Captures are just numbered and you can't get the index from the Capture object
                                    captureId = 0;
                                    foreach (Capture captureiterator in group.Captures)
                                    {
                                        capture = captureiterator;  // Assigned so format() can use the local
                                        captureId += 1;
                                        yield return format();
                                    } // Capture loop
                                } // _detail == Detail.Capture
                            } // Group.Success == true
                        } // Group loop
                    } // _detail == Detail.Group or Detail.Capture
                } // Match loop
            } // _detail == Detail.Match or Detail.Group or Detail.Capture
        } // FormatLineMatches(LineMatches lm)

        private string FormatMatchGroups(Match match)
        {   //-----------------------------------------------------------------
            StringBuilder ret = new StringBuilder(1024);
            
            // Skip the first groupname. It's "0" and represents the match
            foreach (string groupname in _rx.GetGroupNames().Skip(1))
            {
                Group group = match.Groups[groupname];
                if (group.Success)
                {
                    if (ret.Length > 0) ret.Append(" ");
                    ret.Append(groupname).Append(@"=""").Append(group.Value).Append(@"""");
                }
            }
            return ret.ToString();
        }

        private string FormatLine(LineMatches line, 
                                  Match       match   = null,   int    matchindex   = 0, 
                                  Group       group   = null,   string groupname    = null, 
                                  Capture     capture = null,   int    captureindex = 0)
        {   //-----------------------------------------------------------------
            _count += 1;
            string[] parts = (string[])_formatparts.Clone();

            for (int i = parts.GetLowerBound(0); i < parts.GetUpperBound(0); i += 1)
            {
                switch (_formatparts[i])
                {
                    // Constants
                    case Tokens.Percent:          parts[i] = Tokens.EscapeCharacter;      break;
                    case Tokens.Tab:              parts[i] = "\t";                        break;
                    case Tokens.Newline:          parts[i] = Environment.NewLine;         break;
                    case Tokens.Counter:          parts[i] = _count.ToString();           break;
                    
                    // Parameters
                    case Tokens.SourceName:       parts[i] = _source;                     break;
                    
                    // Line information
                    case Tokens.InputLine:        parts[i] = line.SourceLine;             break;
                    case Tokens.LineNumber:       parts[i] = line.LineNumber.ToString();  break;
                    case Tokens.ReplacedLine:     parts[i] = line.ReplacedLine;           break;
                    
                    // Match
                    case Tokens.MatchPosition:    parts[i] = match.Index.ToString();      break;
                    case Tokens.MatchValue:       parts[i] = match.Value;                 break;
                    case Tokens.MatchId:          parts[i] = matchindex.ToString();       break;
                    case Tokens.MatchGroups:      parts[i] = FormatMatchGroups(match);    break;
                    
                    // Group
                    case Tokens.GroupId:          parts[i] = groupname;                   break;
                    case Tokens.GroupValue:       parts[i] = group.Value;                 break;
                    case Tokens.GroupPosition:    parts[i] = group.Index.ToString();      break;
                    
                    // Capture
                    case Tokens.CaptureId:        parts[i] = captureindex.ToString();     break;
                    case Tokens.CaptureValue:     parts[i] = capture.Value;               break;
                    case Tokens.CapturePosition:  parts[i] = capture.Index.ToString();    break;
                }
            }
            return string.Join(string.Empty, parts); 
        }

        public void DebugOut()
        {   //-----------------------------------------------------------------
            Console.WriteLine(@"Output format pattern: " + _format + @"
Output detail level: " + _detail.ToString() + @"
Outputting '" + _selection.ToString() + @"' lines");
            Console.WriteLine();
        }

    }
}
